Panduan komprehensif tentang kata kunci 'infer' TypeScript, menjelaskan cara menggunakannya dengan tipe kondisional untuk ekstraksi dan manipulasi tipe yang kuat, termasuk studi kasus tingkat lanjut.
Menguasai TypeScript Infer: Ekstraksi Tipe Kondisional untuk Manipulasi Tipe Tingkat Lanjut
Sistem tipe TypeScript sangat kuat, memungkinkan pengembang untuk membuat aplikasi yang tangguh dan mudah dipelihara. Salah satu fitur utama yang memungkinkan kekuatan ini adalah kata kunci infer
yang digunakan bersama dengan tipe kondisional. Kombinasi ini menyediakan mekanisme untuk mengekstrak tipe spesifik dari struktur tipe yang kompleks. Postingan blog ini akan membahas secara mendalam tentang kata kunci infer
, menjelaskan fungsionalitasnya dan menampilkan studi kasus tingkat lanjut. Kami akan menjelajahi contoh-contoh praktis yang dapat diterapkan dalam berbagai skenario pengembangan perangkat lunak, dari interaksi API hingga manipulasi struktur data yang kompleks.
Apa itu Tipe Kondisional?
Sebelum kita menyelami infer
, mari kita ulas kembali tipe kondisional secara singkat. Tipe kondisional di TypeScript memungkinkan Anda untuk mendefinisikan sebuah tipe berdasarkan suatu kondisi, mirip dengan operator ternary di JavaScript. Sintaks dasarnya adalah:
T extends U ? X : Y
Ini dibaca sebagai: "Jika tipe T
dapat ditetapkan ke tipe U
, maka tipenya adalah X
; jika tidak, tipenya adalah Y
."
Contoh:
type IsString<T> = T extends string ? true : false;
type StringResult = IsString<string>; // type StringResult = true
type NumberResult = IsString<number>; // type NumberResult = false
Memperkenalkan Kata Kunci infer
Kata kunci infer
digunakan dalam klausa extends
dari sebuah tipe kondisional untuk mendeklarasikan variabel tipe yang dapat diinferensi dari tipe yang sedang diperiksa. Pada intinya, ini memungkinkan Anda untuk "menangkap" bagian dari sebuah tipe untuk digunakan nanti.
Sintaks Dasar:
type MyType<T> = T extends (infer U) ? U : never;
Dalam contoh ini, jika T
dapat ditetapkan ke suatu tipe, TypeScript akan mencoba menginferensi tipe dari U
. Jika inferensi berhasil, tipenya akan menjadi U
; jika tidak, tipenya akan menjadi never
.
Contoh Sederhana Penggunaan infer
1. Menginferensi Tipe Kembalian (Return Type) dari sebuah Fungsi
Kasus penggunaan yang umum adalah menginferensi tipe kembalian dari sebuah fungsi:
type ReturnType<T extends (...args: any) => any> = T extends (...args: any) => infer R ? R : any;
function add(a: number, b: number): number {
return a + b;
}
type AddReturnType = ReturnType<typeof add>; // type AddReturnType = number
function greet(name: string): string {
return `Hello, ${name}!`;
}
type GreetReturnType = ReturnType<typeof greet>; // type GreetReturnType = string
Dalam contoh ini, ReturnType<T>
menerima tipe fungsi T
sebagai masukan. Tipe ini memeriksa apakah T
dapat ditetapkan ke sebuah fungsi yang menerima argumen apa pun dan mengembalikan sebuah nilai. Jika ya, tipe ini menginferensi tipe kembalian sebagai R
dan mengembalikannya. Jika tidak, tipe ini mengembalikan any
.
2. Menginferensi Tipe Elemen Array
Skenario lain yang berguna adalah mengekstrak tipe elemen dari sebuah array:
type ArrayElementType<T> = T extends (infer U)[] ? U : never;
type NumberArrayType = ArrayElementType<number[]>; // type NumberArrayType = number
type StringArrayType = ArrayElementType<string[]>; // type StringArrayType = string
type MixedArrayType = ArrayElementType<(string | number)[]>; // type MixedArrayType = string | number
type NotAnArrayType = ArrayElementType<number>; // type NotAnArrayType = never
Di sini, ArrayElementType<T>
memeriksa apakah T
adalah tipe array. Jika ya, tipe ini menginferensi tipe elemen sebagai U
dan mengembalikannya. Jika tidak, tipe ini mengembalikan never
.
Studi Kasus Lanjutan Penggunaan infer
1. Menginferensi Parameter dari sebuah Konstruktor
Anda dapat menggunakan infer
untuk mengekstrak tipe parameter dari fungsi konstruktor:
type ConstructorParameters<T extends new (...args: any) => any> = T extends new (...args: infer P) => any ? P : never;
class Person {
constructor(public name: string, public age: number) {}
}
type PersonConstructorParams = ConstructorParameters<typeof Person>; // type PersonConstructorParams = [string, number]
class Point {
constructor(public x: number, public y: number) {}
}
type PointConstructorParams = ConstructorParameters<typeof Point>; // type PointConstructorParams = [number, number]
Dalam kasus ini, ConstructorParameters<T>
menerima tipe fungsi konstruktor T
. Tipe ini menginferensi tipe-tipe dari parameter konstruktor sebagai P
dan mengembalikannya sebagai sebuah tuple.
2. Mengekstrak Properti dari Tipe Objek
infer
juga dapat digunakan untuk mengekstrak properti spesifik dari tipe objek menggunakan mapped types dan tipe kondisional:
type PickByType<T, K extends keyof T, U> = {
[P in K as T[P] extends U ? P : never]: T[P];
};
interface User {
id: number;
name: string;
age: number;
email: string;
isActive: boolean;
}
type StringProperties = PickByType<User, keyof User, string>; // type StringProperties = { name: string; email: string; }
type NumberProperties = PickByType<User, keyof User, number>; // type NumberProperties = { id: number; age: number; }
//Sebuah interface yang merepresentasikan koordinat geografis.
interface GeoCoordinates {
latitude: number;
longitude: number;
altitude: number;
country: string;
city: string;
timezone: string;
}
type NumberCoordinateProperties = PickByType<GeoCoordinates, keyof GeoCoordinates, number>; // type NumberCoordinateProperties = { latitude: number; longitude: number; altitude: number; }
Di sini, PickByType<T, K, U>
membuat tipe baru yang hanya menyertakan properti dari T
(dengan kunci di K
) yang nilainya dapat ditetapkan ke tipe U
. Mapped type melakukan iterasi atas kunci-kunci dari T
, dan tipe kondisional menyaring kunci-kunci yang tidak cocok dengan tipe yang ditentukan.
3. Bekerja dengan Promise
Anda dapat menginferensi tipe yang di-resolve dari sebuah Promise
:
type Awaited<T> = T extends Promise<infer U> ? U : T;
async function fetchData(): Promise<string> {
return 'Data from API';
}
type FetchDataType = Awaited<ReturnType<typeof fetchData>>; // type FetchDataType = string
async function fetchNumbers(): Promise<number[]> {
return [1, 2, 3];
}
type FetchedNumbersType = Awaited<ReturnType<typeof fetchNumbers>>; //type FetchedNumbersType = number[]
Tipe Awaited<T>
menerima tipe T
, yang diharapkan berupa sebuah Promise. Tipe ini kemudian menginferensi tipe yang di-resolve U
dari Promise tersebut, dan mengembalikannya. Jika T
bukan sebuah promise, tipe ini mengembalikan T. Ini adalah tipe utilitas bawaan di versi TypeScript yang lebih baru.
4. Mengekstrak Tipe dari Array Promise
Menggabungkan Awaited
dan inferensi tipe array memungkinkan Anda untuk menginferensi tipe yang di-resolve oleh sebuah array Promise. Ini sangat berguna ketika berurusan dengan Promise.all
.
type PromiseArrayReturnType<T extends Promise<any>[]> = {
[K in keyof T]: Awaited<T[K]>;
};
async function getUSDRate(): Promise<number> {
return 0.0069;
}
async function getEURRate(): Promise<number> {
return 0.0064;
}
const rates = [getUSDRate(), getEURRate()];
type RatesType = PromiseArrayReturnType<typeof rates>;
// type RatesType = [number, number]
Contoh ini pertama-tama mendefinisikan dua fungsi asinkron, getUSDRate
dan getEURRate
, yang menyimulasikan pengambilan kurs mata uang. Tipe utilitas PromiseArrayReturnType
kemudian mengekstrak tipe yang di-resolve dari setiap Promise
dalam array, menghasilkan tipe tuple di mana setiap elemen adalah tipe yang ditunggu (awaited) dari Promise yang bersangkutan.
Contoh Praktis di Berbagai Domain
1. Aplikasi E-commerce
Pertimbangkan sebuah aplikasi e-commerce di mana Anda mengambil detail produk dari API. Anda dapat menggunakan infer
untuk mengekstrak tipe data produk:
interface Product {
id: number;
name: string;
price: number;
description: string;
imageUrl: string;
category: string;
rating: number;
countryOfOrigin: string;
}
async function fetchProduct(productId: number): Promise<Product> {
// Simulasi panggilan API
return new Promise((resolve) => {
setTimeout(() => {
resolve({
id: productId,
name: 'Example Product',
price: 29.99,
description: 'A sample product',
imageUrl: 'https://example.com/image.jpg',
category: 'Electronics',
rating: 4.5,
countryOfOrigin: 'Canada'
});
}, 500);
});
}
type ProductType = Awaited<ReturnType<typeof fetchProduct>>; // type ProductType = Product
function displayProductDetails(product: ProductType) {
console.log(`Product Name: ${product.name}`);
console.log(`Price: ${product.price} ${product.countryOfOrigin === 'Canada' ? 'CAD' : (product.countryOfOrigin === 'USA' ? 'USD' : 'EUR')}`);
}
fetchProduct(123).then(displayProductDetails);
Dalam contoh ini, kita mendefinisikan interface Product
dan fungsi fetchProduct
yang mengambil detail produk dari API. Kita menggunakan Awaited
dan ReturnType
untuk mengekstrak tipe Product
dari tipe kembalian fungsi fetchProduct
, memungkinkan kita untuk melakukan pemeriksaan tipe pada fungsi displayProductDetails
.
2. Internasionalisasi (i18n)
Misalkan Anda memiliki fungsi terjemahan yang mengembalikan string yang berbeda berdasarkan lokal. Anda dapat menggunakan infer
untuk mengekstrak tipe kembalian dari fungsi ini untuk keamanan tipe:
interface Translations {
greeting: string;
farewell: string;
welcomeMessage: (name: string) => string;
}
const enTranslations: Translations = {
greeting: 'Hello',
farewell: 'Goodbye',
welcomeMessage: (name: string) => `Welcome, ${name}!`,
};
const frTranslations: Translations = {
greeting: 'Bonjour',
farewell: 'Au revoir',
welcomeMessage: (name: string) => `Bienvenue, ${name}!`,
};
function getTranslation(locale: 'en' | 'fr'): Translations {
return locale === 'en' ? enTranslations : frTranslations;
}
type TranslationType = ReturnType<typeof getTranslation>;
function greetUser(locale: 'en' | 'fr', name: string) {
const translations = getTranslation(locale);
console.log(translations.welcomeMessage(name));
}
greetUser('fr', 'Jean'); // Output: Bienvenue, Jean!
Di sini, TranslationType
diinferensi sebagai interface Translations
, memastikan bahwa fungsi greetUser
memiliki informasi tipe yang benar untuk mengakses string yang diterjemahkan.
3. Penanganan Respons API
Saat bekerja dengan API, struktur respons bisa jadi kompleks. infer
dapat membantu mengekstrak tipe data spesifik dari respons API yang bersarang:
interface ApiResponse<T> {
status: number;
data: T;
message?: string;
}
interface UserData {
id: number;
username: string;
email: string;
profile: {
firstName: string;
lastName: string;
country: string;
language: string;
}
}
async function fetchUser(userId: number): Promise<ApiResponse<UserData>> {
// Simulasi panggilan API
return new Promise((resolve) => {
setTimeout(() => {
resolve({
status: 200,
data: {
id: userId,
username: 'johndoe',
email: 'john.doe@example.com',
profile: {
firstName: 'John',
lastName: 'Doe',
country: 'USA',
language: 'en'
}
}
});
}, 500);
});
}
type UserApiResponse = Awaited<ReturnType<typeof fetchUser>>;
type UserProfileType = UserApiResponse['data']['profile'];
function displayUserProfile(profile: UserProfileType) {
console.log(`Name: ${profile.firstName} ${profile.lastName}`);
console.log(`Country: ${profile.country}`);
}
fetchUser(123).then((response) => {
if (response.status === 200) {
displayUserProfile(response.data.profile);
}
});
Dalam contoh ini, kita mendefinisikan interface ApiResponse
dan interface UserData
. Kita menggunakan infer
dan pengindeksan tipe untuk mengekstrak UserProfileType
dari respons API, memastikan bahwa fungsi displayUserProfile
menerima tipe yang benar.
Praktik Terbaik Menggunakan infer
- Keep it Simple: Gunakan
infer
hanya jika diperlukan. Penggunaan berlebihan dapat membuat kode Anda lebih sulit dibaca dan dipahami. - Document Your Types: Tambahkan komentar untuk menjelaskan apa yang dilakukan oleh tipe kondisional dan pernyataan
infer
Anda. - Test Your Types: Gunakan pemeriksaan tipe TypeScript untuk memastikan bahwa tipe Anda berperilaku seperti yang diharapkan.
- Consider Performance: Tipe kondisional yang kompleks terkadang dapat memengaruhi waktu kompilasi. Perhatikan kompleksitas tipe Anda.
- Use Utility Types: TypeScript menyediakan beberapa tipe utilitas bawaan (misalnya,
ReturnType
,Awaited
) yang dapat menyederhanakan kode Anda dan mengurangi kebutuhan akan pernyataaninfer
kustom.
Kesalahan Umum
- Incorrect Inference: Terkadang, TypeScript mungkin menginferensi tipe yang tidak sesuai dengan yang Anda harapkan. Periksa kembali definisi tipe dan kondisi Anda.
- Circular Dependencies: Berhati-hatilah saat mendefinisikan tipe rekursif menggunakan
infer
, karena dapat menyebabkan dependensi sirkular dan kesalahan kompilasi. - Overly Complex Types: Hindari membuat tipe kondisional yang terlalu kompleks yang sulit dipahami dan dipelihara. Pecah menjadi tipe-tipe yang lebih kecil dan lebih mudah dikelola.
Alternatif untuk infer
Meskipun infer
adalah alat yang kuat, ada situasi di mana pendekatan alternatif mungkin lebih sesuai:
- Type Assertions: Dalam beberapa kasus, Anda dapat menggunakan asersi tipe untuk secara eksplisit menentukan tipe suatu nilai alih-alih menginferensinya. Namun, berhati-hatilah dengan asersi tipe, karena dapat melewati pemeriksaan tipe.
- Type Guards: Type guard dapat digunakan untuk mempersempit tipe suatu nilai berdasarkan pemeriksaan saat runtime. Ini berguna ketika Anda perlu menangani tipe yang berbeda berdasarkan kondisi runtime.
- Utility Types: TypeScript menyediakan seperangkat tipe utilitas yang kaya yang dapat menangani banyak tugas manipulasi tipe umum tanpa perlu pernyataan
infer
kustom.
Kesimpulan
Kata kunci infer
di TypeScript, ketika digabungkan dengan tipe kondisional, membuka kemampuan manipulasi tipe tingkat lanjut. Ini memungkinkan Anda untuk mengekstrak tipe spesifik dari struktur tipe yang kompleks, memungkinkan Anda untuk menulis kode yang lebih tangguh, mudah dipelihara, dan aman secara tipe. Dari menginferensi tipe kembalian fungsi hingga mengekstrak properti dari tipe objek, kemungkinannya sangat luas. Dengan memahami prinsip-prinsip dan praktik terbaik yang diuraikan dalam panduan ini, Anda dapat memanfaatkan infer
secara maksimal dan meningkatkan keterampilan TypeScript Anda. Ingatlah untuk mendokumentasikan tipe Anda, mengujinya secara menyeluruh, dan mempertimbangkan pendekatan alternatif jika sesuai. Menguasai infer
memberdayakan Anda untuk menulis kode TypeScript yang benar-benar ekspresif dan kuat, yang pada akhirnya mengarah pada perangkat lunak yang lebih baik.